//
// GPSMapEdit
// (c) Konstantin Galichsky (kg@geopainting.com), 2002-2005
//
// Export to cGPSmapper.exe.
//

# include "StdAfx.h"
# include "Resource.h"
# include "Save.h"
# include "Types.h"
# include "GarminTypes.h"
# include "Settings.h"
# include "Map.h"
# include "UndoRedo.h"
# include "SaveFileDlg.h"
# include "ShowFile.h"
# include "LinkDlg.h"
# include "ParseHelpers.h"
# include "Wrappers.h"
# include "Globals.h"

static
bool IsNotExecutable (const char * _strPath) {
	CCom com;

	SHFILEINFO fi;
	return ::SHGetFileInfo (_strPath, 0, & fi, sizeof (fi), SHGFI_EXETYPE) == 0;
}

static volatile HANDLE g_hCGPSMapper;
static bool g_bTerminateCGPSMapper;
void Run_cGPSmapper (const char * _strCGPSMapper, bool _bPOIOnly, const char * _strImgFileName, HWND _hOutput);

static WNDPROC g_wpEdit = NULL;

static
LRESULT CALLBACK EditProc (HWND _hWnd, UINT _uMsg, WPARAM _wParam, LPARAM _lParam) {
	if (_uMsg == WM_LBUTTONDBLCLK) {
		// NOTE: _lParam contains client coordinates of click point.
		// Get index of the nearest line in the edit box.
		const DWORD dwLine = HIWORD (::SendMessage (_hWnd, EM_CHARFROMPOS, 0, _lParam));

		// Get beginning part of the line.
		char strLine [MAX_PATH + 10];
		* (reinterpret_cast<WORD *> (strLine)) = sizeof (strLine);
		const size_t cLen = ::SendMessage (_hWnd, EM_GETLINE, dwLine, reinterpret_cast<LPARAM> (strLine));

		const char * pLineRef = NULL;
		for (const char * p = strLine + 2; p < strLine + cLen; ++ p) {
			if (* p == '(')
				pLineRef = p;
			if (* p == ':') {
				const size_t cLineRef = pLineRef != NULL ? ParseDecU (pLineRef + 1) : 0;
				if (cLineRef > 0) {
					const string_t strFileName (strLine, pLineRef);
					const size_t cOffset = cLineRef; // TODO:
					ShowFileLineBox (_hWnd, strFileName.c_str (), cOffset);
				}
				break;
			}
		}

		return 0;
	}

	return ::CallWindowProc (g_wpEdit, _hWnd, _uMsg, _wParam, _lParam);
}

static
INT_PTR CALLBACK CGPSmapperDlgProc (HWND _hWnd, UINT _uMsg, WPARAM _wParam, LPARAM _lParam) {
	const INT_PTR r = LinkDlgProc (_hWnd, _uMsg, _wParam, _lParam);
	if (r)
		return r;

	switch (_uMsg) {
		case WM_INITDIALOG: {
			g_hCGPSMapper = NULL;
			g_bTerminateCGPSMapper = false;

			// Save path to IMG.
			::SetWindowLong (_hWnd, GWL_USERDATA, _lParam);
			
			// Init path to cGPSmapper.exe.
			char strPath [MAX_PATH + 1];
			GetSetting ("cGPSMapper", strPath, MAX_PATH, "");

			const HWND hPath = ::GetDlgItem (_hWnd, IDC_PATH_CGPSMAPPER);
			::SendMessage (hPath, EM_LIMITTEXT, MAX_PATH, 0);
			::SendMessage (hPath, WM_SETTEXT, 0, reinterpret_cast<LPARAM> (strPath));

			// Output.
			const HWND hOutput = ::GetDlgItem (_hWnd, IDC_OUTPUT);
			::SendMessage (hOutput, WM_SETFONT, reinterpret_cast<WPARAM> (g_hFixedFont), FALSE);

			// Subclass the edit box to hook WM_LBUTTONDBLCLK.
			g_wpEdit = reinterpret_cast<WNDPROC> (::GetWindowLong (hOutput, GWL_WNDPROC));
			::SetWindowLong (hOutput, GWL_WNDPROC, reinterpret_cast<long> (EditProc));

			::EnableWindow (::GetDlgItem (_hWnd, ID_RUN), ! IsNotExecutable (strPath));

			return TRUE;
		}

		case WM_CTLCOLORSTATIC: {
			const HDC hDC = reinterpret_cast<HDC> (_wParam);
			const HWND hOutput = ::GetDlgItem (_hWnd, IDC_OUTPUT);
			if (reinterpret_cast<HWND> (_lParam) == hOutput) {
				::SetBkColor (hDC, ::GetSysColor (COLOR_WINDOW));
				return reinterpret_cast<INT_PTR> (::GetSysColorBrush (COLOR_WINDOW));
			}
			return FALSE;
		}

		case WM_COMMAND: {
			switch (LOWORD (_wParam)) {
				case IDC_SELECT_PATH_CGPSMAPPER: {
					char strPath [MAX_PATH + 1];
					::GetDlgItemText (_hWnd, IDC_PATH_CGPSMAPPER, strPath, MAX_PATH);

					OPENFILENAME ofn;
					::memset (& ofn, 0, sizeof (ofn));
					ofn.lStructSize = sizeof (ofn);
					ofn.lpstrFile = strPath;
					ofn.nMaxFile = MAX_PATH;
					ofn.hwndOwner = _hWnd;
					ofn.lpstrFilter  = "Application (*.exe)\0*.exe\0";
					ofn.nFilterIndex = 1;
					ofn.Flags = OFN_ENABLESIZING | OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;
					if (::GetOpenFileName (& ofn)) {
						::SetDlgItemText (_hWnd, IDC_PATH_CGPSMAPPER, strPath);

						SaveSetting ("cGPSMapper", strPath);

						::EnableWindow (::GetDlgItem (_hWnd, ID_RUN), ! IsNotExecutable (strPath));
					}
					break;
				}

				case IDC_PATH_CGPSMAPPER: {
					if (HIWORD (_wParam) == EN_CHANGE) {
						char strPath [MAX_PATH + 1];
						::GetDlgItemText (_hWnd, IDC_PATH_CGPSMAPPER, strPath, MAX_PATH);

						SaveSetting ("cGPSMapper", strPath);

						::EnableWindow (::GetDlgItem (_hWnd, ID_RUN), ! IsNotExecutable (strPath));
					}
					break;
				}

				case ID_RUN: {
					::EnableWindow (::GetDlgItem (_hWnd, IDC_SELECT_PATH_CGPSMAPPER), FALSE);
					::EnableWindow (::GetDlgItem (_hWnd, IDC_PATH_CGPSMAPPER),        FALSE);
					::EnableWindow (::GetDlgItem (_hWnd, IDC_POI_ONLY),               FALSE);
					::EnableWindow (::GetDlgItem (_hWnd, ID_RUN),                     FALSE);

					char strPath [MAX_PATH + 1];
					GetSetting ("cGPSMapper", strPath, MAX_PATH, "");

					const bool bPOIOnly = ::IsDlgButtonChecked (_hWnd, IDC_POI_ONLY) == BST_CHECKED;

					const char * const strImgFileName = reinterpret_cast<const char *> (::GetWindowLong (_hWnd, GWL_USERDATA));

					Run_cGPSmapper (strPath, bPOIOnly, strImgFileName, ::GetDlgItem (_hWnd, IDC_OUTPUT));

					return TRUE;
				}

				case IDCANCEL:
					if (g_hCGPSMapper == NULL) {
						::EndDialog (_hWnd, 0);
						return TRUE;
					}
					// Ask user to confirm termination.
					if (
						YesNoMsgBox (
							"cGPSmapper.exe is still running!\n"
							"Press 'Yes' to terminate it or 'No' to continue."
						) == true
					) {
						if (g_hCGPSMapper) {
							g_bTerminateCGPSMapper = true;
							::TerminateProcess (g_hCGPSMapper, 1);
						}
						::EndDialog (_hWnd, 0);
						return TRUE;
					}
					return TRUE;
			}
			break;
		}
	}

	return FALSE;
}

struct CWaitThreadData {
	DWORD  dwProcessId;
	HWND   hwndOutput;
	HANDLE hRead;
};

static
DWORD WINAPI WaitThreadProc (LPVOID _pData) {
	const std::auto_ptr<CWaitThreadData> pData (reinterpret_cast<CWaitThreadData *> (_pData));

	const CWinHandle hRead (pData->hRead);
	const CWinHandle hProcess = ::OpenProcess (SYNCHRONIZE | PROCESS_QUERY_INFORMATION | PROCESS_TERMINATE, FALSE, pData->dwProcessId);

	// Prepare the output window.
	const HWND hwndOutput = pData->hwndOutput;
	::SendMessage (hwndOutput, EM_LIMITTEXT, 0xFFFF, 0); // NOTE: 0xFFFF is Win9x limitation.

	g_hCGPSMapper = hProcess;

	// Prepare the output buffer.
	const size_t c_cBufSize = 0xFFFF;
	char * const pOutput = reinterpret_cast<char *> (alloca (c_cBufSize*sizeof (char)));
	int cOutputLen = 0;
	int cOutputOffset = 0;
	int cLastLineOffset = 0;

	string_t strPrevLastLine;

	//
	// Translate stdout of cgpsmapper.exe to the output window.
	//

	const DWORD dwStartTime = ::GetTickCount ();

	while (true) {
		char buf [4096];

		// Get amount of data in the pipe's buffer.
		DWORD dwRead = 0;
		::PeekNamedPipe (hRead, NULL, NULL, NULL, & dwRead, NULL);
		if (dwRead == 0)
			dwRead = 1;
		else if (dwRead > sizeof (buf))
			dwRead = sizeof (buf);

		// Read data from the pipe.
		const BOOL bRes = ::ReadFile (hRead, buf, dwRead, & dwRead, NULL);
		if (bRes == FALSE) {
			const DWORD dwErr = ::GetLastError ();
			if (dwErr == ERROR_BROKEN_PIPE)
				break;
		}

		bool bLF = false;

		for (size_t c = 0; c < dwRead; ++ c) {
			const char ch = buf [c];
			switch (ch) {
				case '\r': // Carriage return
					cOutputOffset = cLastLineOffset;
					break;

				case '\n': // New line
				case '\f': // Form feed
				case '\v': // Vertical tab
					pOutput [cOutputLen ++] = '\r';
					pOutput [cOutputLen ++] = '\n';
					cOutputOffset = cOutputLen;
					cLastLineOffset = cOutputLen;
					bLF = true;
					break;

				case '\b': // Backspace
					if (cLastLineOffset < cOutputOffset) {
						-- cOutputOffset;
						cOutputLen = cOutputOffset;
					}
					break;

				case '\a': // Bell (alert)
					::MessageBeep (-1);
					break;

				case '\t': // Horizontal tab
				default:
					pOutput [cOutputOffset] = ch;
					++ cOutputOffset;
					cOutputLen = cOutputOffset;
			}

			if (cOutputLen >= c_cBufSize - 16) {
				// Skip the beginning of buffer to avoid overflow.
				const size_t c_cShiftSize = c_cBufSize/16;
				::memmove (pOutput, pOutput + c_cShiftSize, c_cBufSize - c_cShiftSize);

				cLastLineOffset -= c_cShiftSize;
				if (cLastLineOffset < 0)
					cLastLineOffset = 0;

				cOutputOffset -= c_cShiftSize;
				if (cOutputOffset < 0)
					cOutputOffset = 0;

				cOutputLen -= c_cShiftSize;
				if (cOutputLen < 0)
					cOutputLen = 0;
			}
		}
		pOutput [cOutputLen] = '\0';

		if (bLF) {
			::SendMessage (hwndOutput, WM_SETTEXT, 0, reinterpret_cast<LPARAM> (pOutput));

			::SendMessage (hwndOutput, EM_SETSEL, cOutputLen, cOutputLen);
			::SendMessage (hwndOutput, EM_SCROLLCARET, 0, 0);
		} else {
			string_t strLastLine;
			assert (cLastLineOffset <= cOutputLen);
			if (cOutputLen >= cLastLineOffset)
				strLastLine.assign (pOutput + cLastLineOffset, cOutputLen - cLastLineOffset);

			if (! (strPrevLastLine == strLastLine)) {
				::SendMessage (hwndOutput, EM_SETSEL, cLastLineOffset, cOutputLen);
				::SendMessage (hwndOutput, EM_REPLACESEL, FALSE, reinterpret_cast<LPARAM> (pOutput + cLastLineOffset));
				strPrevLastLine.swap (strLastLine);
			}
		}
	}

	::WaitForSingleObject (hProcess, INFINITE);

	const DWORD dwElapsedTime = ::GetTickCount () - dwStartTime;

	g_hCGPSMapper = NULL;

	//
	// Analyze the results.
	//

	if (g_bTerminateCGPSMapper) {
		InfoMsgBox ("cgpsmapper.exe has terminated by user request.\n");
		return 0;
	}

	DWORD dwExitCode = 0;
	::GetExitCodeProcess (hProcess, & dwExitCode);

	if (dwExitCode == 0) {
		const DWORD dwSeconds = dwElapsedTime / 1000;
		const DWORD dwMinutes = dwSeconds / 60;
		const DWORD dwHours   = dwMinutes / 60;

		InfoMsgBox (
			"Export is complete!\n"
			"Elapsed time: %02dh:%02dm:%02d.%02ds\n",
			dwHours, dwMinutes % 60, dwSeconds % 60, (dwElapsedTime/10) % 100
		);
	} else
		ErrorMsgBox (
			"cgpsmapper.exe has terminated with error code %d.\n"
			"\n"
			"(Please report the problem to the developer of cgpsmapper (www.cgpsmapper.com).)",
			dwExitCode
		);

	return 0;
}

static
void Run_cGPSmapper (const char * _strCGPSMapper, bool _bPOIOnly, const char * _strImgFileName, HWND _hwndOutput) {
	//
	// Prepare command line.
	//
	string_t strCL = "\"";
	strCL.append (_strCGPSMapper);
	if (_bPOIOnly)
		strCL.append ("\" ac -i \"");
	else
		strCL.append ("\" ac \"");
	strCL.append (g_strFileName);
	strCL.append ("\" -o \"");
	strCL.append (_strImgFileName);
	strCL.append ("\"");

	//
	// Launch cGPSMapper.exe
	//

	// Prepare current directory for cGPSmapper.exe
	char strCurrentDir [MAX_PATH + 1];
	char * pFilePart = NULL;
	::GetFullPathName (_strCGPSMapper, MAX_PATH, strCurrentDir, & pFilePart);
	* pFilePart = '\0';

	SECURITY_ATTRIBUTES sa = {sizeof (SECURITY_ATTRIBUTES), NULL, TRUE};
	HANDLE hRead, hWrite;
	::CreatePipe (& hRead, & hWrite, & sa, 0);
	CWinHandle hWrite_Handler (hWrite);

	STARTUPINFO si;
	::memset (& si, 0, sizeof (si));
	si.cb = sizeof (si);
	si.wShowWindow = SW_HIDE;
	si.hStdOutput = hWrite;
	si.hStdError = hWrite;
	si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;

	PROCESS_INFORMATION pi;
	::memset (& pi, 0, sizeof (pi));
	::CreateProcess (NULL, const_cast<char *> (strCL.c_str ()), NULL, NULL, TRUE, BELOW_NORMAL_PRIORITY_CLASS, NULL, strCurrentDir, & si, & pi);
	CWinHandle hProcess (pi.hProcess);
	CWinHandle hThread (pi.hThread);
	if (! hProcess.IsValid ()) {
		ErrorMsgBox ("Failed to run cGPSmapper.exe\n Error code: %d.", ::GetLastError ());
		::FreeConsole ();
	}

	//
	// Run thread waiting the end of export.
	//

	CWaitThreadData * pData = new CWaitThreadData;
	pData->dwProcessId = pi.dwProcessId;
	pData->hwndOutput = _hwndOutput;
	pData->hRead = hRead;

	DWORD dwWaitThreadId = 0;
	CWinHandle hWaitThread (::CreateThread (NULL, 0, WaitThreadProc, pData, 0, & dwWaitThreadId));
}

////////////////////////////

class CGPSMapperExport : public CMapSaver {
  public:
	virtual bool CanSave (const map_t & _map) {return _map.pTypeSet == & g_tsGarmin;}
	virtual bool Save (HANDLE _hFile, const char * _strFileName, const map_t & _map);
};

DECLARE_MAP_EXPORT (CGPSMapperExport, "cgpsmapper", "img", "Garmin IMG / cgpsmapper.exe");

bool CGPSMapperExport::Save (HANDLE _hFile, const char * _strFileName, const map_t & _map) {
	if (! CheckVectorData (_map))
		return false;

	if (_map.pTypeSet != & g_tsGarmin) {
		ErrorMsgBox ("The type set of the map is not 'Garmin'.");
		return false;
	}

	// Make sure that file is saved in Polish format.
	if (g_bModified || g_pSaverInfo == NULL || ::strcmp (g_pSaverInfo->m_strClass, "polish") != 0) {
		ErrorMsgBox ("Please save the map in Polish format before export.");
		return false;
	}

	//
	// Prepare file name.
	//
	char strImgFileName [MAX_PATH + 1];
	::strncpy (strImgFileName, _strFileName, MAX_PATH);
	strImgFileName [MAX_PATH] = '\0';
	if (! SaveFileDlg ("img", "Garmin MapSource map (*.img)\0*.img\0", strImgFileName))
		return false;

	DlgBox (IDD_CGPSMAPPER, g_hWnd, CGPSmapperDlgProc, reinterpret_cast<LPARAM> (strImgFileName));

	return true;
}
